Skip to content

feat(core): add OAuth2 consent flow support - #584

Open
Jorgagu wants to merge 7 commits into
ory:mainfrom
Jorgagu:feat/oauth2-consent-flow
Open

Jorgagu wants to merge 7 commits into
ory:mainfrom
Jorgagu:feat/oauth2-consent-flow

Conversation

@Jorgagu

@Jorgagu Jorgagu commented Jan 5, 2026 •

Copy link
Copy Markdown
Contributor

Add complete OAuth2 consent flow support to @ory/nextjs and @ory/elements-react packages, enabling applications to handle OAuth2 authorization consent screens with Ory Hydra.

Related Issue or Design Document

Fixes #327

Features

  • Consent Flow Utilities (@ory/nextjs)

    • App router: getConsentFlow, acceptConsentRequest, rejectConsentRequest
    • Pages router: getServerSideConsentFlow (for getServerSideProps) and useSession
    • The consent request is always fetched server side, since reading it requires the Hydra admin API and the project API token
    • Accept and reject validate that the session identity matches the consent request subject, preventing consent hijacking with a stolen consent_challenge
  • OAuth2 Client Info Display (@ory/elements-react)

    • Consent card lists requested scopes as grant_scope checkboxes and shows the OAuth2 client name
    • The OAuth2 client logo is displayed on the consent card only (per review feedback, login and registration keep the application's own branding)
  • Example Implementations

    • App Router: consent page + API route
    • Pages Router: consent page + API route
    • Custom Components: ConsentFooter, custom scope checkbox with toggle switches
    • CSRF protection via @csrf-armor/nextjs middleware, plus an explicit double-submit comparison in the consent API routes (the library falls back to the csrf cookie when extracting the submitted token, so the cookie pair alone is not sufficient)
    • Consent pages only render when the session identity matches the consent request subject

Improvements

  • Optimize rewriteUrls to single-pass regex replacement
  • Add OAuth2 path exclusion in URL rewriting
  • Add null/undefined handling in rewriteJsonResponse

Tests

  • Unit tests for consent utilities and rewrite functions

Checklist

  • I have read the contributing guidelines and signed the CLA.
  • I have referenced an issue containing the design document if my change introduces a new feature.
  • I have read the security policy.
  • I confirm that this pull request does not address a security vulnerability.
    If this pull request addresses a security vulnerability,
    I confirm that I got approval (please contact security@ory.sh) from the maintainers to push the changes.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added the necessary documentation within the code base (if appropriate).

Further comments

This implementation follows the pattern established in kratos-selfservice-ui-node for handling OAuth2 flows. The OAuth2 client logo is displayed by overriding the project configuration's logo_light_url when an OAuth2 client logo is available, keeping the existing DefaultCardLogo component unchanged; the override is applied by the consent flow component only.

Summary by CodeRabbit

  • New Features
    • Added OAuth2 consent-flow support for Next.js App Router and Pages Router integrations, including APIs to retrieve, accept, and reject consent requests, plus access to the current session.
    • Consent screens display the OAuth2 client name and logo and support customized scope controls and footer actions.
  • Security
    • Added CSRF protection to consent endpoints and example applications, and checks that the signed-in identity matches the consent request.
  • Bug Fixes
    • Improved URL rewriting for OAuth2 and OpenID Connect paths, including edge cases.

@changeset-bot

changeset-bot Bot commented Jan 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e283498

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jan 5, 2026

Copy link
Copy Markdown

@Jorgagu is attempting to deploy a commit to the ory Team on Vercel.

A member of the Team first needs to authorize it.

@codecov

codecov Bot commented Jan 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.29%. Comparing base (f3fad4d) to head (e283498).
⚠️ Report is 358 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #584       +/-   ##
===========================================
+ Coverage   42.43%   60.29%   +17.86%     
===========================================
  Files         136      188       +52     
  Lines        2008     3833     +1825     
  Branches      288      633      +345     
===========================================
+ Hits          852     2311     +1459     
- Misses       1149     1398      +249     
- Partials        7      124      +117     
Components Coverage Δ
@ory/elements-react 60.23% <ø> (+23.44%) ⬆️
@ory/nextjs 60.59% <ø> (-5.39%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jorgagu Jorgagu changed the title feat(oauth2): add OAuth2 consent flow support feat(core): add OAuth2 consent flow support Jan 5, 2026
@Jorgagu

Jorgagu commented Jan 7, 2026

Copy link
Copy Markdown
Contributor Author

@vinckr @jonas-jonas @aeneasr Happy New Year ! 🎉 Could you please review this one ?

@jonas-jonas

Copy link
Copy Markdown
Member

hi @Jorgagu, thank you very much for this contribution, and happy new year!

We'll take a look at this in the coming weeks. We do have some code for this already; it just wasn't ready to be published, so we might need to do some merging with that.

And just a heads-up, we're quite busy ramping up after the holidays again, so it might take a couple days longer for us to get to this.

@Jorgagu
Jorgagu force-pushed the feat/oauth2-consent-flow branch 3 times, most recently from 70b3e84 to 73020fc Compare February 3, 2026 12:43
@alamchrstn

Copy link
Copy Markdown

hi thanks for this contrib @Jorgagu ! this really helps our team to understand more context on the Ory FE stack here
wondering if we're looking to have this into main anytime soon @jonas-jonas ?

@jonas-jonas jonas-jonas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this contribution! Overall it looks solid. I do have one note about the missing CSRF protection. Happy to discuss this in more detail, if needed!

consentChallenge={consentRequest}
session={session}
config={config}
csrfToken=""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoints used in this component are susceptible to CSRF attacks. This is also the reason the consent screen is not yet part of the elements examples.

Since the <Consent /> component does not yet support server actions, the CSRF protection must be done through middleware. This is kind of awkward to implement, and there aren't many libraries out there that support this.

Ideally, we'd move the Consent screen to a server actions-based form submission because they're CSRF protected by default, but that would make support for the pages router impossible.

https://www.npmjs.com/package/@edge-csrf/nextjs?activeTab=readme is the best library for this (even though it is unmaintained, and vulnerable to subdomain attacks). Could you integrate that into this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jonas-jonas, thanks for the review!

A few things worth noting about the current state:

  • The consent form submits via fetch() with Content-Type: application/json (line 161 of useOryFormSubmit.ts), so cross-origin CSRF is blocked by CORS preflight.
  • I've added server-side session identity validation on both routers so the server checks session.identity.id matches
    consentRequest.subject before processing.
  • The csrfToken="" is not validated anywhere server-side though, which is a gap if the submission mechanism ever changes.

Regarding @edge-csrf/nextjs, it uses the Naive Double-Submit Cookie Pattern (vulnerable to subdomain attacks) and the maintainer is looking to hand off the project (last stable release: October 2024).

Here are the options I see:

Approach Trade-offs
@edge-csrf/nextjs Subdomain vulnerability, uncertain maintenance
Signed Double Submit Cookie (custom) No dependency, works with both routers, no subdomain vulnerability (OWASP recommended)
Custom request header Simplest with fetch(), breaks if we ever switch to HTML form POST
Server Actions CSRF-protected by default, but kills Pages Router support

I'd lean toward the Signed Double Submit Cookie. What's your preference?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the investigation.

Signed double submit seems like the best solution. However, we generally try not to reinvent the wheel (unless necessary!), and CSRF seems like a well-solved issue, so ideally we use a library to solve this. "Don't roll your own security" applies here.

However, when I last looked into this, there were few libraries that actually solved this for Next.js specifically. Do you know of any?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into Next.js CSRF libraries that implement the Signed Double Submit Cookie pattern. Here's what I found:

Package Pattern Version Latest Release Monthly Downloads App Router Pages Router Status
@edge-csrf/nextjs Naive Double Submit v2.5.3 Nov 2024 117,278 Yes Yes Maintainer stepping down, subdomain vulnerability
next-csrf Synchronizer Token (stateful) v0.2.1 Apr 2022 23,608 No Yes Abandoned
@csrf-armor/nextjs Signed Double Submit (HMAC) v1.4.1 Feb 2026 694 Yes Yes Active
@simple-csrf/next Signed Double Submit v0.1.1 Apr 2025 176 Yes No Requires React 19

@csrf-armor/nextjs is the only library that checks all the boxes: Signed Double Submit (HMAC), both routers, Edge
Runtime compatible, zero dependencies, TypeScript, and CodeQL analysis. The adoption is low, but the code is auditable
and the core package (@csrf-armor/core) is framework-agnostic, which
could allow building a Nuxt adapter on top of it for the Vue/Nuxt side of ory-elements in the future.

What do you think about going with @csrf-armor/nextjs?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the compilation. I think that's right; let's use @csrf-armor/nextjs here. Would you mind adjusting the examples to use the package?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonas-jonas its done, could you please try it and give me review on it ?

@Jorgagu
Jorgagu force-pushed the feat/oauth2-consent-flow branch 3 times, most recently from 0afc868 to a4ba0fd Compare March 9, 2026 20:57
@alexanderNthl

Copy link
Copy Markdown

hey @Jorgagu, thanks a lot for your work on this, it really helps!
I noticed that the Consent flow is not fully compatible with internationalization (i18n) yet:

  • The UINodes in oauth2.ts have conflicting IDs (4x 9999111) and are not part of the known messages yet.
  • The footer contains hard coded messages.

Do you think it make sense to include i18n for the consent flow in this PR or should this be part of a new issue/PR? Thanks in advance for your answer.

@Jorgagu

Jorgagu commented Mar 14, 2026

Copy link
Copy Markdown
Contributor Author

Good catch @alexanderNthl, you're right the consent flow doesn't have proper i18n yet.

I looked into it and the 9999111 ID reused across all nodes in oauth2.ts was just a placeholder, and the footer strings are hardcoded in English.

Kratos already has numeric ID ranges per flow (1010xxx for Login, 1040xxx for Registration, up to 1080xxx Verification). I checked and the 1090xxx range is completely free, so I'd use that for consent:

  • 1090001: "Accept"
  • 1090002: "Reject"
  • 1090003: "Remember my decision"
  • 1090004: "Make sure you trust {client}"
  • 1090005: "You may be sharing sensitive information with this site or application."
  • 1090006: "Authorizing will redirect to {client}"

The existing string keys (consent.title, consent.subtitle, consent.scope.*) are already fine.

@jonas-jonas do you think I should include this in the current PR or handle it separately?

@jonas-jonas jonas-jonas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more comment; otherwise, this looks good! Could you resolve the merge conflicts once more?

Comment on lines +63 to +66
const configWithLogo = getConfigWithOAuth2Logo(
config,
flow.oauth2_login_request?.client?.logo_uri,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct. The login screen is for the main application (think Google, Facebook, etc.), whereas with this change, it would render as the client logo (e.g., My ACME app). This would be very confusing to end users ("Am I logging in with my ACME app account or my google account?").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The client logo override no longer applies to the login and registration screens, and the client name subtitles are gone from constructCardHeader.ts as well: login.tsx, registration.tsx and constructCardHeader.ts are now identical to main. The OAuth2 client logo is only used on the consent screen, through getConfigWithOAuth2Logo in flows/consent.tsx.

@jonas-jonas

Copy link
Copy Markdown
Member

@Jorgagu The message IDs issue has to be addressed; it's on our list internally, but it's out of scope of this PR, as the component already existed before.

@Jorgagu
Jorgagu force-pushed the feat/oauth2-consent-flow branch from a4ba0fd to 7ec7ef5 Compare July 20, 2026 21:43
@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2ff4370a-ae7f-4e50-901b-f66e1f650af9

📥 Commits

Reviewing files that changed from the base of the PR and between d47992e and e283498.


📒 Files selected for processing (5)
  • examples/nextjs-app-router-custom-components/app/api/consent/route.ts
  • examples/nextjs-app-router/app/api/consent/route.ts
  • examples/nextjs-pages-router/pages/api/consent.ts
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/src/utils/rewrite.ts

🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/src/utils/rewrite.ts

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.



📝 Walkthrough

Walkthrough

Adds OAuth2 consent APIs for Next.js App and Pages Routers, consent-specific Elements rendering, CSRF-protected examples, client branding support, and OAuth2-aware URL rewriting.

Changes

OAuth2 consent flow

Layer / File(s) Summary
Next.js consent APIs
packages/nextjs/src/app/..., packages/nextjs/src/pages/..., packages/nextjs/api-report/..., .changeset/oauth2-consent-flow.md
Adds consent retrieval and accept/reject helpers, server and client OAuth2 clients, a Pages Router session hook, public exports, tests, and API declarations.
Consent UI rendering
packages/elements-react/src/components/..., packages/elements-react/src/theme/default/...
Separates footer nodes, renders consent inputs, applies OAuth2 client logos, and tests consent node classification and keys.
App Router consent examples
examples/nextjs-app-router/..., examples/nextjs-app-router-custom-components/...
Adds consent pages and API processing with session and subject checks, CSRF validation, request parsing, custom consent controls, and middleware routing.
Pages Router consent example
examples/nextjs-pages-router/...
Adds server-side consent retrieval, session validation, API handling, consent rendering, and CSRF middleware.
OAuth2 URL rewrite handling
packages/nextjs/src/utils/...
Preserves OAuth2/OIDC URLs during rewriting, handles nullish JSON inputs, and expands rewrite tests.

Repository housekeeping

Layer / File(s) Summary
Coverage ignore rule
.gitignore
Updates the ignore rule for the coverage/ directory.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ConsentPage
  participant NextjsConsentAPI
  participant OAuth2Api
  Browser->>ConsentPage: request consent challenge
  ConsentPage->>NextjsConsentAPI: load consent request and session
  NextjsConsentAPI->>OAuth2Api: fetch consent request
  OAuth2Api-->>NextjsConsentAPI: return consent request
  ConsentPage-->>Browser: render consent form
  Browser->>NextjsConsentAPI: submit CSRF-protected decision
  NextjsConsentAPI->>OAuth2Api: verify subject and submit consent decision
  OAuth2Api-->>NextjsConsentAPI: return redirect_to
Loading

Merge Risk: ⚪ Minimal · up to e2834

The consent flow’s submitted choices map consistently to the provider’s decisions, with the consent subject checked. No actionable merge risk introduced by this change is established.

Pre-merge checks | Passed 3 | Failed 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check Warning The .gitignore change adds coverage/. The change has no direct connection to Hydra consent support, package integration, examples, or required tests. The other reported changes support the consent… Remove the unrelated coverage/ entry, or provide a direct feature requirement that requires this ignore rule.
Docstring Coverage Warning Docstring coverage is 38.89% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 36 functions across 23 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: adding OAuth2 consent flow support.
Description check Passed The description is complete and follows the repository template. It explains the feature, references issue #327, lists implementation details and tests, completes the checklist, and includes further c…
Linked Issues check Passed Issue #327 requires Hydra login and consent support in the Next.js examples and @ory/nextjs. Existing login routes remain present. This pull request adds App Router and Pages Router consent retrieva…

Full details: Out of Scope Changes check

Explanation

The .gitignore change adds coverage/. The change has no direct connection to Hydra consent support, package integration, examples, or required tests. The other reported changes support the consent implementation or its security and test coverage.


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Create a new PR


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Jorgagu

Jorgagu commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@jonas-jonas the branch is rebased on the current main and the merge conflicts are resolved.

Changes since your last review:

  • The OAuth2 client logo override no longer applies to the login and registration screens, it is now used by the consent flow component only. I also removed the client-name subtitles from constructCardHeader.ts for the same reason, so login and registration keep the application's own branding entirely.
  • The pages router client-side hook useConsentFlow was replaced by a server-side getServerSideConsentFlow for getServerSideProps: fetching the consent request requires the Hydra admin API and the project API token, so it must never run in the browser.
  • CSRF: the examples use @csrf-armor/nextjs as agreed. One note: in the current release (1.4.4), the token extraction falls back to the csrf cookie itself when neither the header nor the body carries it, which would make the middleware comparison self-referential for the consent form submit. The example consent API routes therefore add an explicit double-submit comparison between the submitted csrf_token field and the cookie. This looks worth an upstream issue.
  • The consent pages only render the consent request when the session identity matches the request subject, mirroring the existing accept/reject validation.
  • Added a changeset (minor for @ory/elements-react and @ory/nextjs).

The PR description is updated accordingly.

@Jorgagu
Jorgagu requested a review from jonas-jonas July 20, 2026 21:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
packages/nextjs/src/pages/client.ts (1)

27-29: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Remove redundant and unsafe window.location.origin access.

guessPotentiallyProxiedOrySdkUrl already safely defaults to window.location.origin when running on the client. Accessing window.location.origin unconditionally here bypasses that safety and will throw a ReferenceError if this factory is ever accidentally invoked during Server-Side Rendering (SSR).

♻️ Proposed refactor
-      basePath: guessPotentiallyProxiedOrySdkUrl({
-        knownProxiedUrl: window.location.origin,
-      }),
+      basePath: guessPotentiallyProxiedOrySdkUrl(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/pages/client.ts` around lines 27 - 29, Remove the
explicit knownProxiedUrl value from the guessPotentiallyProxiedOrySdkUrl call in
the client factory, allowing the helper to apply its safe client-side default
without unconditionally accessing window during SSR.
packages/nextjs/src/pages/consent.ts (1)

76-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider logging the error before swallowing it.

Returning null on failure handles invalid challenges gracefully, but silently swallowing all errors (such as 500s or misconfigured API keys) can make debugging difficult. Consider logging the error to help developers identify configuration or network issues.

♻️ Proposed refactor
   return serverSideOAuth2Client()
     .getOAuth2ConsentRequest({ consentChallenge })
-    .catch(() => null)
+    .catch((error) => {
+      console.error("Failed to fetch OAuth2 consent request:", error)
+      return null
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/pages/consent.ts` around lines 76 - 79, Update the OAuth2
consent request flow in serverSideOAuth2Client().getOAuth2ConsentRequest to log
the caught error before returning null. Preserve the existing graceful null
fallback while ensuring failures, including configuration and network errors,
include useful error details in the established logging mechanism.
packages/nextjs/src/pages/session.ts (1)

47-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify state updates with finally.

You can use a .finally() block to handle setting the loading state, reducing duplication across the success and error paths.

♻️ Proposed refactor
       .then((session) => {
         setSession(session)
-        setLoading(false)
-        return
       })
       .catch((err: unknown) => {
         setError(err instanceof Error ? err : new Error(String(err)))
-        setLoading(false)
+      })
+      .finally(() => {
+        setLoading(false)
       })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/pages/session.ts` around lines 47 - 55, Refactor the
promise chain in the session-loading flow to move setLoading(false) from both
the success and error handlers into a shared finally block. Keep setSession in
the success handler and setError in the catch handler, preserving their existing
behavior.
packages/nextjs/src/app/consent.test.ts (1)

82-161: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add tests for the identity-match/mismatch branch.

No test in either describe block passes identityId, so the "Forbidden: Session identity does not match..." guard (the security-critical part of both functions) is never exercised. The mocks here also don't stub getOAuth2ConsentRequest, so this branch would currently throw if invoked.

Add cases for: (1) matching identityId proceeds to accept/reject, (2) mismatched identityId throws.

Also applies to: 163-208

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/app/consent.test.ts` around lines 82 - 161, Extend both
consent test describe blocks covering acceptConsentRequest and
rejectConsentRequest with identityId cases: stub getOAuth2ConsentRequest with a
request identity for each test, verify matching identityId proceeds to the
underlying accept/reject call, and verify mismatched identityId throws the
forbidden error without proceeding. Keep assertions focused on the security
guard and ensure the mocks support the lookup path.
packages/elements-react/src/theme/default/utils/oauth2-config.ts (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a relative import for internal types.

Importing from the package's own public entry point (@ory/elements-react) from within its internal source code can cause module resolution or circular dependency issues during the build step. Please use a relative path instead.

♻️ Proposed fix
-import { OryClientConfiguration } from "`@ory/elements-react`"
+import { OryClientConfiguration } from "../../../util/clientConfiguration"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/elements-react/src/theme/default/utils/oauth2-config.ts` at line 4,
Update the OryClientConfiguration import in oauth2-config.ts to use the
appropriate relative internal source path instead of the package public entry
point, preserving the existing type usage.
packages/nextjs/src/utils/rewrite.ts (1)

82-85: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Handle top-level arrays safely to prevent type conversion issues.

If obj is an Array (e.g., a top-level JSON array response), allowing it to fall through to Object.fromEntries(Object.entries(obj)...) will incorrectly convert it into an Object with stringified numeric keys (e.g., {"0": item1, "1": item2}).

Consider adding early handling for arrays to prevent accidental data corruption.

♻️ Proposed refactor
   // Handle null/undefined input to prevent runtime errors
   if (!obj) {
     return obj
   }
+  if (Array.isArray(obj)) {
+    return obj
+      .map((item) =>
+        typeof item === "object" && item !== null
+          ? rewriteJsonResponse(item, proxyUrl)
+          : typeof item === "string" && proxyUrl
+            ? item.replaceAll(orySdkUrl(), proxyUrl)
+            : item,
+      )
+      .filter((item) => item !== undefined) as unknown as T
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/utils/rewrite.ts` around lines 82 - 85, Update the
null/undefined guard in the object-rewrite logic to detect top-level arrays and
return them unchanged before the Object.fromEntries(Object.entries(...))
conversion. Preserve the existing handling for non-array objects and nullish
inputs.
examples/nextjs-app-router/middleware.ts (1)

34-51: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add export const config = { matcher: [...] } to scope middleware execution.

Neither middleware file defines a matcher, so per Next.js defaults this middleware (CSRF processing + Ory path routing) runs on every request, including static assets, images, and favicon — unlike the @csrf-armor/nextjs documented setup, which explicitly excludes _next/static, _next/image, and favicon.ico.

  • examples/nextjs-app-router/middleware.ts#L34-L51: add a matcher export excluding static asset paths.
  • examples/nextjs-app-router-custom-components/middleware.ts#L34-L51: apply the same addition.
♻️ Proposed addition
   const response = NextResponse.next()
   const result = await csrfProtect(request, response)
   if (!result.success) {
     return NextResponse.json(
       { error: "CSRF validation failed" },
       { status: 403 },
     )
   }
   return response
 }
+
+export const config = {
+  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/nextjs-app-router/middleware.ts` around lines 34 - 51, Add an
exported Next.js middleware config with a matcher that excludes _next/static,
_next/image, and favicon.ico from execution. Apply the same configuration
alongside middleware in examples/nextjs-app-router/middleware.ts and
examples/nextjs-app-router-custom-components/middleware.ts; leave the middleware
request handling unchanged.
examples/nextjs-app-router/app/api/consent/route.ts (1)

82-114: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate action explicitly instead of defaulting unknown values to reject.

Any body.action other than exactly "accept" (including missing/malformed values) silently falls through to rejectConsentRequest. This fails safe (denies rather than grants), but it hides malformed-request bugs as spurious consent denials instead of a clear 400.

♻️ Proposed validation
+  if (action !== "accept" && action !== "reject") {
+    return NextResponse.json(
+      { error: "invalid_request", error_description: "Invalid or missing action" },
+      { status: 400 },
+    )
+  }
+
   if (!consentChallenge) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/nextjs-app-router/app/api/consent/route.ts` around lines 82 - 114,
Validate body.action explicitly before the consent operation: accept only
“accept” or “reject”, and return a 400 invalid_request response for missing or
any other value. Update the branching around acceptConsentRequest and
rejectConsentRequest so rejectConsentRequest is called only when action is
exactly “reject”.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/nextjs-app-router/middleware.ts`:
- Around line 42-50: Return the protected response from csrfProtect instead of
the original response. Update the success path in
examples/nextjs-app-router/middleware.ts (lines 42-50) and
examples/nextjs-app-router-custom-components/middleware.ts (lines 42-50) to
return result.response, while preserving the existing CSRF failure response.

In `@examples/nextjs-pages-router/pages/auth/consent.tsx`:
- Around line 12-51: Update getServerSideProps to read and decode the CSRF token
from the request cookie, include it in ConsentPageProps, and pass it through the
returned props. Update ConsentPage to use the prop instead of
getCsrfTokenFromCookie, then remove that client-side helper.

In `@packages/nextjs/src/app/consent.ts`:
- Around line 112-156: Make identity validation mandatory in
acceptConsentRequest by requiring identityId in its options and always
retrieving the consent request to verify its subject matches identityId before
accepting it. Apply the same requirement and validation to rejectConsentRequest,
preferably by extracting and reusing an assertConsentSubject helper so both
consent flows enforce identical checks.

In `@packages/nextjs/src/utils/rewrite.ts`:
- Around line 43-63: Update the URL regex in the rewrite logic around the regex
construction to require a valid domain boundary after baseUrlNormalized,
preventing matches inside longer hostnames. In the callback’s OAuth2 and UI
mapping checks, replace raw path.startsWith checks with boundary-aware matching
so only the exact path or a path beginning with the configured segment followed
by a separator is rewritten.

---

Nitpick comments:
In `@examples/nextjs-app-router/app/api/consent/route.ts`:
- Around line 82-114: Validate body.action explicitly before the consent
operation: accept only “accept” or “reject”, and return a 400 invalid_request
response for missing or any other value. Update the branching around
acceptConsentRequest and rejectConsentRequest so rejectConsentRequest is called
only when action is exactly “reject”.

In `@examples/nextjs-app-router/middleware.ts`:
- Around line 34-51: Add an exported Next.js middleware config with a matcher
that excludes _next/static, _next/image, and favicon.ico from execution. Apply
the same configuration alongside middleware in
examples/nextjs-app-router/middleware.ts and
examples/nextjs-app-router-custom-components/middleware.ts; leave the middleware
request handling unchanged.

In `@packages/elements-react/src/theme/default/utils/oauth2-config.ts`:
- Line 4: Update the OryClientConfiguration import in oauth2-config.ts to use
the appropriate relative internal source path instead of the package public
entry point, preserving the existing type usage.

In `@packages/nextjs/src/app/consent.test.ts`:
- Around line 82-161: Extend both consent test describe blocks covering
acceptConsentRequest and rejectConsentRequest with identityId cases: stub
getOAuth2ConsentRequest with a request identity for each test, verify matching
identityId proceeds to the underlying accept/reject call, and verify mismatched
identityId throws the forbidden error without proceeding. Keep assertions
focused on the security guard and ensure the mocks support the lookup path.

In `@packages/nextjs/src/pages/client.ts`:
- Around line 27-29: Remove the explicit knownProxiedUrl value from the
guessPotentiallyProxiedOrySdkUrl call in the client factory, allowing the helper
to apply its safe client-side default without unconditionally accessing window
during SSR.

In `@packages/nextjs/src/pages/consent.ts`:
- Around line 76-79: Update the OAuth2 consent request flow in
serverSideOAuth2Client().getOAuth2ConsentRequest to log the caught error before
returning null. Preserve the existing graceful null fallback while ensuring
failures, including configuration and network errors, include useful error
details in the established logging mechanism.

In `@packages/nextjs/src/pages/session.ts`:
- Around line 47-55: Refactor the promise chain in the session-loading flow to
move setLoading(false) from both the success and error handlers into a shared
finally block. Keep setSession in the success handler and setError in the catch
handler, preserving their existing behavior.

In `@packages/nextjs/src/utils/rewrite.ts`:
- Around line 82-85: Update the null/undefined guard in the object-rewrite logic
to detect top-level arrays and return them unchanged before the
Object.fromEntries(Object.entries(...)) conversion. Preserve the existing
handling for non-array objects and nullish inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 225bf684-e938-4b95-b2fa-4640b5eced41

📥 Commits

Reviewing files that changed from the base of the PR and between 11d7877 and 7ec7ef5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (42)
  • .changeset/oauth2-consent-flow.md
  • .gitignore
  • examples/nextjs-app-router-custom-components/.env
  • examples/nextjs-app-router-custom-components/app/api/consent/route.ts
  • examples/nextjs-app-router-custom-components/app/auth/consent/consent-form.tsx
  • examples/nextjs-app-router-custom-components/app/auth/consent/page.tsx
  • examples/nextjs-app-router-custom-components/components/consent-utils.ts
  • examples/nextjs-app-router-custom-components/components/custom-consent-scope-checkbox.tsx
  • examples/nextjs-app-router-custom-components/components/custom-footer.tsx
  • examples/nextjs-app-router-custom-components/components/index.tsx
  • examples/nextjs-app-router-custom-components/middleware.ts
  • examples/nextjs-app-router-custom-components/package.json
  • examples/nextjs-app-router/.env
  • examples/nextjs-app-router/app/api/consent/route.ts
  • examples/nextjs-app-router/app/auth/consent/consent-form.tsx
  • examples/nextjs-app-router/app/auth/consent/page.tsx
  • examples/nextjs-app-router/middleware.ts
  • examples/nextjs-app-router/package.json
  • examples/nextjs-pages-router/.env
  • examples/nextjs-pages-router/middleware.ts
  • examples/nextjs-pages-router/package.json
  • examples/nextjs-pages-router/pages/api/consent.ts
  • examples/nextjs-pages-router/pages/auth/consent.tsx
  • packages/elements-react/src/components/card/card-consent.test.ts
  • packages/elements-react/src/components/card/card-consent.tsx
  • packages/elements-react/src/components/form/nodes/input.tsx
  • packages/elements-react/src/components/form/nodes/node-button.tsx
  • packages/elements-react/src/theme/default/flows/consent.tsx
  • packages/elements-react/src/theme/default/utils/oauth2-config.ts
  • packages/elements-react/src/theme/default/utils/oauth2.ts
  • packages/nextjs/api-report/nextjs-client.api.json
  • packages/nextjs/api-report/nextjs.api.md
  • packages/nextjs/src/app/client.ts
  • packages/nextjs/src/app/consent.test.ts
  • packages/nextjs/src/app/consent.ts
  • packages/nextjs/src/app/index.ts
  • packages/nextjs/src/pages/client.ts
  • packages/nextjs/src/pages/consent.ts
  • packages/nextjs/src/pages/index.ts
  • packages/nextjs/src/pages/session.ts
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/src/utils/rewrite.ts
💤 Files with no reviewable changes (1)
  • packages/elements-react/src/components/form/nodes/node-button.tsx

Comment thread examples/nextjs-app-router/middleware.ts Outdated
Comment thread examples/nextjs-pages-router/pages/auth/consent.tsx
Comment thread packages/nextjs/src/app/consent.ts
Comment thread packages/nextjs/src/utils/rewrite.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nextjs/src/utils/rewrite.ts`:
- Around line 56-60: In the uiPathMappings loop, replace the
String.prototype.replace call with string concatenation using path.slice so the
configUrl value is never interpreted for replacement patterns. Preserve the
existing matched uiPath prefix and append the unchanged suffix after uiPath.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fcd8702f-078b-4c16-a113-6bb5e54a802e

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec7ef5 and 9fb70db.

📒 Files selected for processing (9)
  • examples/nextjs-app-router-custom-components/middleware.ts
  • examples/nextjs-app-router/middleware.ts
  • examples/nextjs-pages-router/middleware.ts
  • packages/nextjs/api-report/nextjs-client.api.json
  • packages/nextjs/api-report/nextjs.api.md
  • packages/nextjs/src/app/consent.test.ts
  • packages/nextjs/src/app/consent.ts
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/src/utils/rewrite.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • examples/nextjs-pages-router/middleware.ts
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/api-report/nextjs.api.md
  • examples/nextjs-app-router/middleware.ts
  • examples/nextjs-app-router-custom-components/middleware.ts
  • packages/nextjs/src/app/consent.test.ts

Comment thread packages/nextjs/src/utils/rewrite.ts
Add getConsentFlow, acceptConsentRequest and rejectConsentRequest for
the app router, and getServerSideConsentFlow plus useSession for the
pages router. The consent request is always fetched server side since
reading it requires the Hydra admin API and the project API token.

Accepting and rejecting validate that the session identity matches the
consent request subject to prevent consent hijacking with a stolen
consent_challenge. URL rewriting is consent-aware in rewriteUrls.
The consent card lists the requested scopes as grant_scope checkboxes
and shows the OAuth2 client name. The client logo override only applies
to the consent screen: per review feedback, login and registration keep
the application's own branding.
Add consent pages and API routes to the app router, custom components,
and pages router examples. CSRF protection uses the @csrf-armor/nextjs
middleware plus an explicit double-submit comparison in the consent
routes, since the library falls back to the csrf cookie when extracting
the submitted token. Middleware path matching is aligned with the
@ory/nextjs proxy match list, and the consent pages only render when
the session identity matches the consent request subject.
Make identityId mandatory in acceptConsentRequest and
rejectConsentRequest so the subject check cannot be skipped by omission,
via a shared assertConsentSubject helper.

Harden rewriteUrls against prefix collisions: a lookahead enforces a
domain boundary so hostnames merely sharing the base URL prefix are left
untouched, and UI path overrides only match whole path segments so
"/login" no longer captures "/login-methods".
Return result.response from csrfProtect instead of the pre-protection
response. The current adapter mutates the passed response in place, but
the processed response is the documented contract and stays correct if
that implementation detail changes.
@Jorgagu
Jorgagu force-pushed the feat/oauth2-consent-flow branch from 9fb70db to d47992e Compare October 11, 2026 14:47
String.prototype.replace expands $& and $' in the replacement string, so a
configured UI URL containing them was altered. Concatenate the override URL
with the remaining path instead.
The consent API routes treated any action other than accept as a rejection.
Answer 400 for values other than accept and reject instead.
@Jorgagu

Jorgagu commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

@jonas-jonas the branch is rebased on the current main again and the merge conflicts are resolved.

Since my last comment:

  • rewriteUrls no longer passes the configured UI URL to String.prototype.replace, so $& or $' in a configured URL stay literal (follow-up of the last automated review).
  • The example consent API routes answer 400 for an action other than accept or reject, instead of treating it as a rejection.

Your two review threads are addressed: the examples use @csrf-armor/nextjs, and the client logo is only rendered on the consent screen. Could you take another look when you have time?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Ory Hydra login and consent features to Next.js examples and Next.js package

5 participants